"use strict";
import emitter from "contra/emitter";
import crossvent from "crossvent";
import classes from "./classes";
// var emitter = require('contra/emitter');
// var crossvent = require('crossvent');
// var classes = require('./classes');
var doc = document;
var documentElement = doc.documentElement;

function dragula(initialContainers, options) {
	var len = arguments.length;
	if (len === 1 && Array.isArray(initialContainers) === false) {
		options = initialContainers;
		initialContainers = [];
	}
	var _mirror; // mirror image
	var _source; // source container
	var _item; // item being dragged
	var _offsetX; // reference x
	var _offsetY; // reference y
	var _moveX; // reference move x
	var _moveY; // reference move y
	var _initialSibling; // reference sibling when grabbed
	var _currentSibling; // reference sibling now
	var _copy; // item used for copying
	var _renderTimer; // timer for setTimeout renderMirrorImage
	var _lastDropTarget = null; // last container item was over
	var _grabbed; // holds mousedown context until first mousemove

	var o = options || {};
	if (o.moves === void 0) {
		o.moves = always;
	}
	if (o.accepts === void 0) {
		o.accepts = always;
	}
	if (o.invalid === void 0) {
		o.invalid = invalidTarget;
	}
	if (o.containers === void 0) {
		o.containers = initialContainers || [];
	}
	if (o.isContainer === void 0) {
		o.isContainer = never;
	}
	if (o.copy === void 0) {
		o.copy = false;
	}
	if (o.copySortSource === void 0) {
		o.copySortSource = false;
	}
	if (o.revertOnSpill === void 0) {
		o.revertOnSpill = false;
	}
	if (o.removeOnSpill === void 0) {
		o.removeOnSpill = false;
	}
	if (o.direction === void 0) {
		o.direction = "vertical";
	}
	if (o.ignoreInputTextSelection === void 0) {
		o.ignoreInputTextSelection = true;
	}
	if (o.mirrorContainer === void 0) {
		o.mirrorContainer = doc.body;
	}

	var drake = emitter({
		containers: o.containers,
		start: manualStart,
		end: end,
		cancel: cancel,
		remove: remove,
		destroy: destroy,
		canMove: canMove,
		dragging: false,
	});

	if (o.removeOnSpill === true) {
		drake.on("over", spillOver).on("out", spillOut);
	}

	events();

	return drake;

	function isContainer(el) {
		return drake.containers.indexOf(el) !== -1 || o.isContainer(el);
	}

	function events(remove) {
		var op = remove ? "remove" : "add";
		touchy(documentElement, op, "mousedown", grab);
		touchy(documentElement, op, "mouseup", release);
	}

	function eventualMovements(remove) {
		var op = remove ? "remove" : "add";
		touchy(documentElement, op, "mousemove", startBecauseMouseMoved);
	}

	function movements(remove) {
		var op = remove ? "remove" : "add";
		crossvent[op](documentElement, "selectstart", preventGrabbed); // IE8
		crossvent[op](documentElement, "click", preventGrabbed);
	}

	function destroy() {
		events(true);
		release({});
	}

	function preventGrabbed(e) {
		if (_grabbed) {
			e.preventDefault();
		}
	}

	function grab(e) {
		_moveX = e.clientX;
		_moveY = e.clientY;

		var ignore = whichMouseButton(e) !== 1 || e.metaKey || e.ctrlKey;
		if (ignore) {
			return; // we only care about honest-to-god left clicks and touch events
		}
		var item = e.target;
		var context = canStart(item);
		if (!context) {
			return;
		}
		_grabbed = context;
		eventualMovements();
		if (e.type === "mousedown") {
			if (isInput(item)) {
				// see also: https://github.com/bevacqua/dragula/issues/208
				item.focus(); // fixes https://github.com/bevacqua/dragula/issues/176
			} else {
				e.preventDefault(); // fixes https://github.com/bevacqua/dragula/issues/155
			}
		}
	}

	function startBecauseMouseMoved(e) {
		if (!_grabbed) {
			return;
		}
		if (whichMouseButton(e) === 0) {
			release({});
			return; // when text is selected on an input and then dragged, mouseup doesn't fire. this is our only hope
		}
		// truthy check fixes #239, equality fixes #207
		if (e.clientX !== void 0 && e.clientX === _moveX && e.clientY !== void 0 && e.clientY === _moveY) {
			return;
		}
		if (o.ignoreInputTextSelection) {
			var clientX = getCoord("clientX", e);
			var clientY = getCoord("clientY", e);
			var elementBehindCursor = doc.elementFromPoint(clientX, clientY);
			if (isInput(elementBehindCursor)) {
				return;
			}
		}

		var grabbed = _grabbed; // call to end() unsets _grabbed
		eventualMovements(true);
		movements();
		end();
		start(grabbed);

		var offset = getOffset(_item);
		_offsetX = getCoord("pageX", e) - offset.left;
		_offsetY = getCoord("pageY", e) - offset.top;

		classes.add(_copy || _item, "gu-transit");
		renderMirrorImage();
		drag(e);
	}

	function canStart(item) {
		if (drake.dragging && _mirror) {
			return;
		}
		if (isContainer(item)) {
			return; // don't drag container itself
		}
		var handle = item;
		while (getParent(item) && isContainer(getParent(item)) === false) {
			if (o.invalid(item, handle)) {
				return;
			}
			item = getParent(item); // drag target should be a top element
			if (!item) {
				return;
			}
		}
		var source = getParent(item);
		if (!source) {
			return;
		}
		if (o.invalid(item, handle)) {
			return;
		}

		var movable = o.moves(item, source, handle, nextEl(item));
		if (!movable) {
			return;
		}

		return {
			item: item,
			source: source,
		};
	}

	function canMove(item) {
		return !!canStart(item);
	}

	function manualStart(item) {
		var context = canStart(item);
		if (context) {
			start(context);
		}
	}

	function start(context) {
		if (isCopy(context.item, context.source)) {
			_copy = context.item.cloneNode(true);
			drake.emit("cloned", _copy, context.item, "copy");
		}

		_source = context.source;
		_item = context.item;
		_initialSibling = _currentSibling = nextEl(context.item);

		drake.dragging = true;
		drake.emit("drag", _item, _source);
	}

	function invalidTarget() {
		return false;
	}

	function end() {
		if (!drake.dragging) {
			return;
		}
		var item = _copy || _item;
		drop(item, getParent(item));
	}

	function ungrab() {
		_grabbed = false;
		eventualMovements(true);
		movements(true);
	}

	function release(e) {
		ungrab();

		if (!drake.dragging) {
			return;
		}
		var item = _copy || _item;
		var clientX = getCoord("clientX", e);
		var clientY = getCoord("clientY", e);
		var elementBehindCursor = getElementBehindPoint(_mirror, clientX, clientY);
		var dropTarget = findDropTarget(elementBehindCursor, clientX, clientY);
		if (dropTarget && ((_copy && o.copySortSource) || (!_copy || dropTarget !== _source))) {
			drop(item, dropTarget);
		} else if (o.removeOnSpill) {
			remove();
		} else {
			cancel();
		}
	}

	function drop(item, target) {
		var parent = getParent(item);
		if (_copy && o.copySortSource && target === _source) {
			parent.removeChild(_item);
		}
		if (isInitialPlacement(target)) {
			drake.emit("cancel", item, _source, _source);
		} else {
			drake.emit("drop", item, target, _source, _currentSibling);
		}
		cleanup();
	}

	function remove() {
		if (!drake.dragging) {
			return;
		}
		var item = _copy || _item;
		var parent = getParent(item);
		if (parent) {
			parent.removeChild(item);
		}
		drake.emit(_copy ? "cancel" : "remove", item, parent, _source);
		cleanup();
	}

	function cancel(revert) {
		if (!drake.dragging) {
			return;
		}
		var reverts = arguments.length > 0 ? revert : o.revertOnSpill;
		var item = _copy || _item;
		var parent = getParent(item);
		var initial = isInitialPlacement(parent);
		if (initial === false && reverts) {
			if (_copy) {
				if (parent) {
					parent.removeChild(_copy);
				}
			} else {
				_source.insertBefore(item, _initialSibling);
			}
		}
		if (initial || reverts) {
			drake.emit("cancel", item, _source, _source);
		} else {
			drake.emit("drop", item, parent, _source, _currentSibling);
		}
		cleanup();
	}

	function cleanup() {
		var item = _copy || _item;
		ungrab();
		removeMirrorImage();
		if (item) {
			classes.rm(item, "gu-transit");
		}
		if (_renderTimer) {
			clearTimeout(_renderTimer);
		}
		drake.dragging = false;
		if (_lastDropTarget) {
			drake.emit("out", item, _lastDropTarget, _source);
		}
		drake.emit("dragend", item);
		_source = _item = _copy = _initialSibling = _currentSibling = _renderTimer = _lastDropTarget = null;
	}

	function isInitialPlacement(target, s) {
		var sibling;
		if (s !== void 0) {
			sibling = s;
		} else if (_mirror) {
			sibling = _currentSibling;
		} else {
			sibling = nextEl(_copy || _item);
		}
		return target === _source && sibling === _initialSibling;
	}

	function findDropTarget(elementBehindCursor, clientX, clientY) {
		var target = elementBehindCursor;
		while (target && !accepted()) {
			target = getParent(target);
		}
		return target;

		function accepted() {
			var droppable = isContainer(target);
			if (droppable === false) {
				return false;
			}

			var immediate = getImmediateChild(target, elementBehindCursor);
			var reference = getReference(target, immediate, clientX, clientY);
			var initial = isInitialPlacement(target, reference);
			if (initial) {
				return true; // should always be able to drop it right back where it was
			}
			return o.accepts(_item, target, _source, reference);
		}
	}

	function drag(e) {
		if (!_mirror) {
			return;
		}
		e.preventDefault();

		var clientX = getCoord("clientX", e);
		var clientY = getCoord("clientY", e);
		var x = clientX - _offsetX;
		var y = clientY - _offsetY;

		_mirror.style.left = x + "px";
		_mirror.style.top = y + "px";

		var item = _copy || _item;
		var elementBehindCursor = getElementBehindPoint(_mirror, clientX, clientY);
		var dropTarget = findDropTarget(elementBehindCursor, clientX, clientY);
		var changed = dropTarget !== null && dropTarget !== _lastDropTarget;
		if (changed || dropTarget === null) {
			out();
			_lastDropTarget = dropTarget;
			over();
		}
		var parent = getParent(item);
		if (dropTarget === _source && _copy && !o.copySortSource) {
			if (parent) {
				parent.removeChild(item);
			}
			return;
		}
		var reference;
		var immediate = getImmediateChild(dropTarget, elementBehindCursor);
		if (immediate !== null) {
			reference = getReference(dropTarget, immediate, clientX, clientY);
		} else if (o.revertOnSpill === true && !_copy) {
			reference = _initialSibling;
			dropTarget = _source;
		} else {
			if (_copy && parent) {
				parent.removeChild(item);
			}
			return;
		}
		if ((reference === null && changed) || (reference !== item && reference !== nextEl(item))) {
			_currentSibling = reference;
			dropTarget.insertBefore(item, reference);
			drake.emit("shadow", item, dropTarget, _source);
		}
		function moved(type) {
			drake.emit(type, item, _lastDropTarget, _source);
		}
		function over() {
			if (changed) {
				moved("over");
			}
		}
		function out() {
			if (_lastDropTarget) {
				moved("out");
			}
		}
	}

	function spillOver(el) {
		classes.rm(el, "gu-hide");
	}

	function spillOut(el) {
		if (drake.dragging) {
			classes.add(el, "gu-hide");
		}
	}

	function renderMirrorImage() {
		if (_mirror) {
			return;
		}
		var rect = _item.getBoundingClientRect();
		_mirror = _item.cloneNode(true);
		_mirror.style.width = getRectWidth(rect) + "px";
		_mirror.style.height = getRectHeight(rect) + "px";
		classes.rm(_mirror, "gu-transit");
		classes.add(_mirror, "gu-mirror");
		o.mirrorContainer.appendChild(_mirror);
		touchy(documentElement, "add", "mousemove", drag);
		classes.add(o.mirrorContainer, "gu-unselectable");
		drake.emit("cloned", _mirror, _item, "mirror");
	}

	function removeMirrorImage() {
		if (_mirror) {
			classes.rm(o.mirrorContainer, "gu-unselectable");
			touchy(documentElement, "remove", "mousemove", drag);
			getParent(_mirror).removeChild(_mirror);
			_mirror = null;
		}
	}

	function getImmediateChild(dropTarget, target) {
		var immediate = target;
		while (immediate !== dropTarget && getParent(immediate) !== dropTarget) {
			immediate = getParent(immediate);
		}
		if (immediate === documentElement) {
			return null;
		}
		return immediate;
	}

	function getReference(dropTarget, target, x, y) {
		var horizontal = o.direction === "horizontal";
		var mixed = o.direction === "mixed";
		var reference = target !== dropTarget ? inside() : outside();
		return reference;

		function outside() {
			// slower, but able to figure out any position
			var len = dropTarget.children.length;
			var i;
			var el;
			var rect;
			for (i = 0; i < len; i++) {
				el = dropTarget.children[i];
				rect = el.getBoundingClientRect();
				if (horizontal && rect.left + rect.width / 2 > x) {
					return el;
				}
				if (!horizontal && rect.top + rect.height / 2 > y) {
					return el;
				}
			}
			return null;
		}

		function inside() {
			// faster, but only available if dropped inside a child element
			var rect = target.getBoundingClientRect();
			if (mixed) {
				var distToTop = y - rect.top;
				var distToLeft = x - rect.left;
				var distToBottom = rect.bottom - y;
				var distToRight = rect.right - x;
				var minDist = Math.min(distToLeft, distToRight, distToTop, distToBottom);
				return resolve(distToLeft === minDist || distToTop === minDist);
			}
			if (horizontal) {
				return resolve(x > rect.left + getRectWidth(rect) / 2);
			}
			return resolve(y > rect.top + getRectHeight(rect) / 2);
		}

		function resolve(after) {
			return after ? nextEl(target) : target;
		}
	}

	function isCopy(item, container) {
		return typeof o.copy === "boolean" ? o.copy : o.copy(item, container);
	}
}

function touchy(el, op, type, fn) {
	var touch = {
		mouseup: "touchend",
		mousedown: "touchstart",
		mousemove: "touchmove",
	};
	var pointers = {
		mouseup: "pointerup",
		mousedown: "pointerdown",
		mousemove: "pointermove",
	};
	var microsoft = {
		mouseup: "MSPointerUp",
		mousedown: "MSPointerDown",
		mousemove: "MSPointerMove",
	};
	if (global.navigator.pointerEnabled) {
		crossvent[op](el, pointers[type], fn);
	} else if (global.navigator.msPointerEnabled) {
		crossvent[op](el, microsoft[type], fn);
	} else {
		crossvent[op](el, touch[type], fn);
		crossvent[op](el, type, fn);
	}
}

function whichMouseButton(e) {
	if (e.touches !== void 0) {
		return e.touches.length;
	}
	if (e.which !== void 0 && e.which !== 0) {
		return e.which;
	} // see https://github.com/bevacqua/dragula/issues/261
	if (e.buttons !== void 0) {
		return e.buttons;
	}
	var button = e.button;
	if (button !== void 0) {
		// see https://github.com/jquery/jquery/blob/99e8ff1baa7ae341e94bb89c3e84570c7c3ad9ea/src/event.js#L573-L575
		return button & 1 ? 1 : button & 2 ? 3 : button & 4 ? 2 : 0;
	}
}

function getOffset(el) {
	var rect = el.getBoundingClientRect();
	return {
		left: rect.left + getScroll("scrollLeft", "pageXOffset"),
		top: rect.top + getScroll("scrollTop", "pageYOffset"),
	};
}

function getScroll(scrollProp, offsetProp) {
	if (typeof global[offsetProp] !== "undefined") {
		return global[offsetProp];
	}
	if (documentElement.clientHeight) {
		return documentElement[scrollProp];
	}
	return doc.body[scrollProp];
}

function getElementBehindPoint(point, x, y) {
	var p = point || {};
	var state = p.className;
	var el;
	p.className += " gu-hide";
	el = doc.elementFromPoint(x, y);
	p.className = state;
	return el;
}

function never() {
	return false;
}
function always() {
	return true;
}
function getRectWidth(rect) {
	return rect.width || rect.right - rect.left;
}
function getRectHeight(rect) {
	return rect.height || rect.bottom - rect.top;
}
function getParent(el) {
	return el.parentNode === doc ? null : el.parentNode;
}
function isInput(el) {
	return el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.tagName === "SELECT" || isEditable(el);
}
function isEditable(el) {
	if (!el) {
		return false;
	} // no parents were editable
	if (el.contentEditable === "false") {
		return false;
	} // stop the lookup
	if (el.contentEditable === "true") {
		return true;
	} // found a contentEditable element in the chain
	return isEditable(getParent(el)); // contentEditable is set to 'inherit'
}

function nextEl(el) {
	return el.nextElementSibling || manually();
	function manually() {
		var sibling = el;
		do {
			sibling = sibling.nextSibling;
		} while (sibling && sibling.nodeType !== 1);
		return sibling;
	}
}

function getEventHost(e) {
	// on touchend event, we have to use `e.changedTouches`
	// see http://stackoverflow.com/questions/7192563/touchend-event-properties
	// see https://github.com/bevacqua/dragula/issues/34
	if (e.targetTouches && e.targetTouches.length) {
		return e.targetTouches[0];
	}
	if (e.changedTouches && e.changedTouches.length) {
		return e.changedTouches[0];
	}
	return e;
}

function getCoord(coord, e) {
	var host = getEventHost(e);
	var missMap = {
		pageX: "clientX", // IE8
		pageY: "clientY", // IE8
	};
	if (coord in missMap && !(coord in host) && missMap[coord] in host) {
		coord = missMap[coord];
	}
	return host[coord];
}

export default dragula;
